| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- import { ReactNode } from 'react';
- import Link from 'next/link';
- import { permanentRedirect } from 'next/navigation';
- import { fetchJson } from '@/lib/utils/server';
- import { ResultDto } from '@/types/response/common';
- import { ChannelDetail } from '@/types/channel';
- import { buildChannelUrl, buildWatchUrl, formatHandle } from '@/lib/utils/channel';
- import FollowButton from '@/app/component/FollowButton';
- import NoteButton from '@/app/(main)/note/_components/NoteButton';
- import PoweredByYouTube from '@/app/component/PoweredByYouTube';
- import ChannelTabs from './_component/ChannelTabs';
- import ChannelNotFound from './_component/ChannelNotFound';
- import './style.scss';
- type Props = {
- children: ReactNode;
- params: Promise<{ identifier: string }>;
- };
- function formatCount(n: number): string {
- if (n >= 10000) {
- return `${(n / 10000).toFixed(n >= 100000 ? 0 : 1)}만`;
- }
- if (n >= 1000) {
- return `${(n / 1000).toFixed(1)}천`;
- }
- return n.toLocaleString();
- }
- export default async function ChannelLayout({ children, params }: Props)
- {
- const { identifier } = await params;
- const decoded = decodeURIComponent(identifier);
- const res: ResultDto<ChannelDetail> = await fetchJson(`/api/channel/${encodeURIComponent(decoded)}`, {
- method: 'GET'
- });
- if (!res.data) {
- return <ChannelNotFound />;
- }
- const ch = res.data;
- // canonical URL 리다이렉트: 현재 URL 파라미터와 실제 채널의 canonical 핸들이 다르면 301
- const canonical = buildChannelUrl(ch);
- const current = `/channel/${decoded}`;
- if (current !== canonical) {
- permanentRedirect(canonical);
- }
- return (
- <div className="channel-page">
- {/* 배너 */}
- <div className="channel-page__banner">
- {ch.bannerUrl && (
- <img
- src={`${ch.bannerUrl}=w1707-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj`}
- alt={`${ch.name} 배너`}
- className="channel-page__banner-img"
- />
- )}
- </div>
- {/* 프로필 */}
- <div className="channel-page__profile">
- {/* 썸네일 */}
- <a href={ch.youTubeUrl} target="_blank" rel="noopener noreferrer" className={`channel-page__avatar${ch.isLive ? ' channel-page__avatar--live' : ''}`}>
- {ch.thumbnailUrl ? (
- <img src={ch.thumbnailUrl} alt={ch.name} />
- ) : (
- <div className="channel-page__avatar-placeholder">{ch.name.charAt(0)}</div>
- )}
- {ch.isLive && <span className="channel-page__live-badge">LIVE</span>}
- </a>
- {/* 채널명 + 메타 */}
- <div className="channel-page__info">
- <h1 className="channel-page__name">
- <a href={ch.youTubeUrl} target="_blank" rel="noopener noreferrer">{ch.name}</a>
- {ch.isVerified && <span className="channel-page__verified" title="인증됨">✓</span>}
- </h1>
- <div className="channel-page__meta">
- {ch.handle && <span>{formatHandle(ch.handle)}</span>}
- {(ch.subscriberCount ?? 0) > 0 && <span>구독자 {formatCount(ch.subscriberCount)}명</span>}
- {(ch.videoCount ?? 0) > 0 && <span>동영상 {ch.videoCount.toLocaleString()}개</span>}
- <PoweredByYouTube />
- </div>
- {/* 데스크톱 전용: 메타 아래 인라인 */}
- <div className="channel-page__buttons channel-page__buttons--desktop">
- <a href={ch.youTubeUrl} target="_blank" rel="noopener noreferrer" className="channel-page__action channel-page__subscribe-btn">구독</a>
- <FollowButton memberSID={ch.memberSID} className="channel-page__action channel-page__follow" />
- <NoteButton
- target={{ memberID: ch.memberID, displayName: ch.name, thumbnailUrl: ch.thumbnailUrl }}
- className="channel-page__action channel-page__note-btn"
- />
- </div>
- </div>
- </div>
- {/* 모바일 전용: 프로필 아래 별도 줄 */}
- <div className="channel-page__buttons channel-page__buttons--mobile mt-3">
- <a href={ch.youTubeUrl} target="_blank" rel="noopener noreferrer" className="channel-page__action channel-page__subscribe-btn">구독</a>
- <FollowButton memberSID={ch.memberSID} className="channel-page__action channel-page__follow" />
- <NoteButton
- target={{ memberID: ch.memberID, displayName: ch.name, thumbnailUrl: ch.thumbnailUrl }}
- className="channel-page__action channel-page__note-btn"
- />
- </div>
- {/* 라이브 상태 */}
- {ch.isLive && (
- <div className="channel-page__live">
- <span className="channel-page__live-dot" />
- <span className="channel-page__live-title">{ch.liveTitle}</span>
- <Link href={buildWatchUrl(ch)} className="channel-page__watch-btn">방송 보러가기 →</Link>
- </div>
- )}
- {/* 탭 네비게이션 */}
- <ChannelTabs identifier={decoded} />
- {/* 탭 콘텐츠 */}
- {children}
- </div>
- );
- }
|